/* 
Copyright 2011 by Mark Coletti, Keith Sullivan, Sean Luke, and
George Mason University Mason University Licensed under the Academic
Free License version 3.0

See the file "LICENSE" for more information
*/
/**
 * GeometryUtilities.java
 * 
 * $Id: GeometryUtilities.java,v 1.4 2010-09-03 22:14:36 mcoletti Exp $
 * 
 */
package sim.util.geo;

import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Comparator;

import sim.portrayal.DrawInfo2D;
import com.vividsolutions.jts.geom.Envelope;
import sim.display.Display2D;
import sim.field.geo.GeomField;
import sim.field.geo.GeomGridField;

public class GeometryUtilities
{

	/**
	 * Determines the affine transform which converts world coordinates into
	 * screen coordinates. Modified from GeoTools RenderUtilities.java.
	 * 
	 * @param mapExtent
	 *            MBR in world coordinates mapped to viewport defined in 'info'
	 * @param viewport
	 * @return AffineTransform suitable for converting from world to screen
	 *         coordinates
	 */
	public static AffineTransform worldToScreenTransform(final Envelope mapExtent,
			final java.awt.geom.Rectangle2D.Double viewport)
	{
		double scaleX = viewport.width / mapExtent.getWidth();
		double scaleY = viewport.height / mapExtent.getHeight();

		double tx = -mapExtent.getMinX() * scaleX;
		double ty = (mapExtent.getMinY() * scaleY) + viewport.height;

		AffineTransform at = new AffineTransform(scaleX, 0.0d, 0.0d, -scaleY, tx, ty);
		AffineTransform originTranslation = AffineTransform.getTranslateInstance(viewport.x, viewport.y);
		originTranslation.concatenate(at);

		return originTranslation != null ? originTranslation : at;

	}

	public static com.vividsolutions.jts.geom.util.AffineTransformation getPortrayalTransform(final AffineTransform transform, 
			final GeomField field,
			final Rectangle2D.Double view)
	{
		AffineTransform worldToScreen = transform; 
		if (worldToScreen.getScaleX() > 1 || worldToScreen.getScaleY() > 1) { 
			Envelope e = new Envelope(field.drawX, field.getFieldWidth(), field.drawY, field.getFieldHeight());
			worldToScreen = worldToScreenTransform(e, view);
		}
		double m[] = new double[6];
		worldToScreen.getMatrix(m);
		return new com.vividsolutions.jts.geom.util.AffineTransformation(m[0], m[2], m[4], m[1], m[3], m[5]);
	}

	/**
	 * Determines the affine transform which converts world coordinates into
	 * screen coordinates. Modified from GeoTools RenderUtilities.java.
	 * 
	 * convenience variant of other worldToSceenTransform()
	 * 
	 * @param mapExtent
	 *            MBR in world coordinates mapped to viewport defined in 'info'
	 * @param info
	 *            defines the viewport dimensions
	 * @return AffineTransform suitable for converting from world to screen
	 *         coordinates
	 */
	public static AffineTransform worldToScreenTransform(final Envelope mapExtent, final DrawInfo2D info)
	{
		return worldToScreenTransform(mapExtent, info.draw);
	}

	/**
	 * Uses the worldToScreen transform to transform the point (x,y) in screen
	 * coordinates to world coordinates.
	 */
	public static Point2D screenToWorldPointTransform(final AffineTransform worldToScreen, double x, double y)
	{
		// code taken from GeoTools and hacked on
		AffineTransform screenToWorld = null;
		try
		{
			screenToWorld = worldToScreen.createInverse();
		} catch (Exception e)
		{
			System.out.println(e);
			System.exit(-1);
		}

		Point2D p = new Point2D.Double();
		screenToWorld.transform(new Point2D.Double(x, y), p);
		return p;
	}

	/**
	 * Comparator to sort and search for AttributeFields by name in a Collection
	 */
	public static Comparator<AttributeField> attrFieldCompartor = new Comparator<AttributeField>()
	{
		public int compare(AttributeField af1, AttributeField af2)
		{
			return af1.name.compareTo(af2.name);
		}
	};

	/**
	 * compute the MBR for the grid field in display coordinates
	 * 
	 * This is used to determine the display bounds for the grid field for
	 * Display2D.attach().
	 * 
	 * @param outer
	 *            denotes MBR that maps to display window in world coordinates
	 * @param display
	 *            is the display into which the grid will be rendered
	 * @param gridField
	 *            for which we wish to find bounds in display coordinates
	 * 
	 * @return grid field bounds in display coordinates; will return viewport if
	 *         grid does not intersect given outer MBR
	 */
	static public java.awt.geom.Rectangle2D.Double computeBounds(final Envelope outer, final Display2D display,
			final GeomGridField gridField)
	{
		Display2D.InnerDisplay2D innerDisplay = display.insideDisplay;

		// Initialize bounds to that of display viewport
		java.awt.geom.Rectangle2D.Double bounds = new java.awt.geom.Rectangle2D.Double(innerDisplay.xOffset,
				innerDisplay.yOffset, innerDisplay.width, innerDisplay.height);

		AffineTransform transform = GeometryUtilities.worldToScreenTransform(outer, bounds);

		if (outer.contains(gridField.MBR) || gridField.MBR.contains(outer) || outer.intersects(gridField.MBR))
		{
			// Pretty straightforward; just translate all the corners into
			// display coordinates

			Point2D.Double srcMinPoint = new Point2D.Double(gridField.MBR.getMinX(), gridField.MBR.getMaxY());
			Point2D destMinPoint = transform.transform(srcMinPoint, null);

			Point2D.Double srcMaxPoint = new Point2D.Double(gridField.MBR.getMaxX(), gridField.MBR.getMinY());
			Point2D destMaxPoint = transform.transform(srcMaxPoint, null);

			bounds.setRect(destMinPoint.getX(), destMinPoint.getY(), destMaxPoint.getX() - destMinPoint.getX(),
					destMaxPoint.getY() - destMinPoint.getY());

		}
		else
		// badness happened
		{
			// not good if the grid isn't even within the outer MBR; this likely
			// means that
			// 'outer' and 'gridField' are using different spatial reference
			// systems
			System.err.println("Warning: raster not in display");
		}

		return bounds;
	}

	/**
	 * @param outer
	 *            denotes MBR that maps to display window in world coordinates
	 * @param gridField
	 *            for which we wish to find bounds in display coordinates
	 * 
	 * @returns true iff 'gridField' is within, intersects, or covers 'outer',
	 *          else returns false
	 * 
	 *          Can be used as check for computeBounds()
	 */
	static public boolean isWithinBounds(final Envelope outer, final GeomGridField gridField)
	{
		if (outer.contains(gridField.MBR) || gridField.MBR.contains(outer) || outer.intersects(gridField.MBR))
		{
			return true;
		}
		return false;
	}

}
